/** 工具对象: util
 * 基于 common.js, storage.js
 * 注1: 如果使用加密方法，需要对应引入 jsrsasign-rsa-min@10.5.17.min.js / keyutil@1.0.min.js / crypto-js@4.1.1.min.js
 * 注2: 如果使用生成二维码方法，需要对应引入 qrcode@1.5.0.js，如果需要支持汉字模式，需引入 qrcode.tosjis@1.5.0.js
 */
(function (root, factory) {
  if (typeof exports === 'object') {
    module.exports = factory(require('storage'));
  } else if (typeof define === 'function' && define.amd) {
    define(['storage'], factory);
  } else {
    root.util = factory(root.storage);
  }
})(this, function (storage) {
  var UA = window.navigator.userAgent.toLowerCase();

  var isAndroid = UA.indexOf('android') > -1 || UA.indexOf('adr') > -1; // android终端
  var isIOS = /\(i[^;]+;( u;)? cpu.+mac os x/.test(UA); // ios终端
  var isIE = /msie|trident/.test(UA);
  var isIE9 = UA.indexOf('msie 9.0') > 0;
  var isEdge = UA.indexOf('edge/') > 0;
  var isChrome = /chrome\/\d+/.test(UA) && !isEdge;
  var isPhantomJS = /phantomjs/.test(UA);
  var isFF = UA.match(/firefox\/(\d+)/);
  var isWechat = /micromessenger/.test(UA);
  var isPcWechat = isWechat && /windowswechat/.test(UA);
  var isAlipay = /alipay/.test(UA);

  // base64编码表，不是ascii编码表
  const base64Map = { "0": 52, "1": 53, "2": 54, "3": 55, "4": 56, "5": 57, "6": 58, "7": 59, "8": 60, "9": 61, "A": 0, "B": 1, "C": 2, "D": 3, "E": 4, "F": 5, "G": 6, "H": 7, "I": 8, "J": 9, "K": 10, "L": 11, "M": 12, "N": 13, "O": 14, "P": 15, "Q": 16, "R": 17, "S": 18, "T": 19, "U": 20, "V": 21, "W": 22, "X": 23, "Y": 24, "Z": 25, "a": 26, "b": 27, "c": 28, "d": 29, "e": 30, "f": 31, "g": 32, "h": 33, "i": 34, "j": 35, "k": 36, "l": 37, "m": 38, "n": 39, "o": 40, "p": 41, "q": 42, "r": 43, "s": 44, "t": 45, "u": 46, "v": 47, "w": 48, "x": 49, "y": 50, "z": 51, "+": 62, "/": 63 };

  const pageUtil = {
    loadFnList: [],
    /**
     * 页面加载完毕执行
     */
    domContentLoaded() {
      const fnList = [];
      for (let i = 0; i < pageUtil.loadFnList.length; i++) {
        if (pageUtil.loadFnList[i].only) {
          fnList.length = 0;
          fnList.push(pageUtil.loadFnList[i].fn);
          break;
        }
        fnList.push(pageUtil.loadFnList[i].fn);
      }
      fnList.forEach((fn) => {
        fn();
      });
    },
    /**
     * 页面读取完成执行
     * @param {Function} fn 加载完成后调用的方法
     * @param {boolean} only 仅调用本次方法
     */
    pageReady(fn, only = false) {
      if (
        document.readyState === 'complete' ||
        (document.readyState !== 'loading' && !document.documentElement.doScroll)) {
        fn();
      } else {
        if (typeof fn === 'function') this.loadFnList.push({ fn, only });
        if (document.addEventListener) {
          document.addEventListener('DOMContentLoaded', this.domContentLoaded, false);
        } else {
          window.onload = this.domContentLoaded;
        }
      }
    },
    /**
     * 识别url参数中的 []、[xx] 等 对象/数组类关键字文本
     * @param {object|undefined} obj
     * @param {array} matches 正则匹配项
     * @param {string} value 目标值
     * @returns {any}
     */
    parseQuery(obj, matches, value = '') {
      const item = matches.shift();
      if (typeof item === 'undefined') {
        return null;
      }
      let k = item[1];
      const kType = k === '' ? 'auto_arr' : (k.match(/^[0-9]+$/) ? 'arr' : 'obj');
      if (typeof obj === 'undefined') {
        obj = kType === 'obj' ? {} : [];
      } else if (typeof obj === 'string') {
        obj = kType === 'obj' ? { 0: obj } : [obj];
      }

      // 先转换 空key 的值
      if (kType === 'auto_arr') {
        k = obj.length;
      }

      // 如果长度大于0，进入迭代，否则说明是最后一次，则直接使用 value
      obj[k] = matches.length > 0 ? this.parseQuery(obj[k], matches, value) : value;
      return obj;
    },
    /**
     * 获取地址栏参数
     * @param {string} key
     * @param {string} url 地址
     * @returns {string | undefined}
     */
    getUrlQuery(key, url = location.search) {
      return this.getUrlQueryObj(url)[key];
    },
    /**
     * 获取地址栏参数，如果不存在则尝试从sessionStorage中获取，并缓存在sessionStorage中
     * @param {string} key
     * @param {string} cacheKey
     * @returns {string | undefined}
     */
    getUrlQuerySession(key, cacheKey) {
      let res = this.getUrlQuery(key);
      if (!res && cacheKey) {
        res = storage.getSession(cacheKey);
      }
      if (res) {
        storage.setSession(cacheKey, res);
      }
      return res;
    },
    /**
     * 识别url链接参数，转换成对象（能自动识别字符串、对象、数组）
     * @param {string} url 链接
     * @returns {object}
     */
    getUrlQueryObj(url = location.search) {
      // 对search字符串进行解码
      url = decodeURI(url);
      const obj = {};
      const queryIdx = url.indexOf('?');
      const queryData = queryIdx !== -1 ? url.substring(queryIdx + 1) : '';
      if (queryData !== '') {
        const arr = queryData.split('&');
        arr.forEach((v) => {
          const idx = v.indexOf('=');
          if (idx !== -1) {
            const key = v.substring(0, idx);
            const value = v.substring(idx + 1);
            const matches = key.match(/\[([^\]]*)](?=\[|$)/g);
            const k = !matches || matches.length === 0 ? key : key.substring(0, matches[0].index);
            obj[k] = !matches || matches.length === 0 ? value : this.parseQuery(obj[k], matches, value);
          }
        });
      }
      return obj;
    },
    /**
     * 解析识别 url 中的各种参数
     * @param {string} url
     * @returns {Array | null}
     */
    parseUrl(url) {
      const reg = /^((?:(^https?):(?:\/\/)?)?((?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+)(?::(\d+))?|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((\/[+~%\/.\w-_]*)?\??([-+=&;%@.\w_]*)?#?([-+=&;%@.\w_]*)?)$/;
      // 0:完整链接
      // 1:域名（包含协议、端口）
      // 2:协议
      // 3:域名
      // 4:端口
      // 5:后续完整路径（包含路径、query、hash）
      // 6:路径
      // 7:参数
      // 8:哈希
      return url.match(reg);
    },
    /**
     * 获取微信环境
     * @returns {number}
     */
    checkWechatOS() {
      return isWechat ? (isPcWechat ? 1 : 2) : 0;
    },
    /**
     * 获取支付宝环境
     * @returns {number}
     */
    checkAlipayOS() {
      return isAlipay ? 1 : 0;
    },
    /**
     * 获取系统环境
     * @returns {string}
     */
    checkOS() {
      if (isAndroid) return 'android';
      if (isIOS) return 'ios';
      return 'other';
    },
    /**
     * 重写 location.replace (安卓 webview 不支持 location.replace)
     * @param {string} url 同源url
     */
    locationReplace(url) {
      let f = true;
      if (url.indexOf('http:') === 0 || url.indexOf('https:') === 0) {
        const firstIdx = url.substring(0, 5) === 'https' ? 8 : 7;
        if (url.substring(firstIdx).indexOf(location.host) !== 0) {
          f = false;
        }
      }
      const isIOS = this.checkOS() === 'ios'; //ios终端
      if (!isIOS && f && history.replaceState) {
        history.replaceState(null, document.title, url);
        history.go(0);
      } else {
        location.replace(url);
      }
    },
    /**
     * 不同系统下，关闭浏览器
     */
    closeWindow() {
      try {
        const isWechat = this.checkWechatOS();
        const isAlipay = this.checkAlipayOS();
        const os = this.checkOS();
        if (isWechat) {
          WeixinJSBridge && WeixinJSBridge.call('closeWindow');
        } else if (isAlipay) {
          AlipayJSBridge && AlipayJSBridge.call('popWindow');
        } else if (os === 'android') {
          window.control &&
          window.control.closewindow &&
          window.control.closewindow();
        } else if (os === 'ios') {
          const bridge = window.webkit && window.webkit.messageHandlers ? window.webkit.messageHandlers.jsBridge : null;
          if (bridge) {
            bridge.postMessage({
              method: 'closewindow',
            });
          } else {
            // 无参调用
            window.WebViewJavascriptBridge &&
            window.WebViewJavascriptBridge.closewindow &&
            window.WebViewJavascriptBridge.closewindow()('closewindow', function (responseData) {
              // console.log('JS received response:', responseData);
            });
          }
        } else {
          window.close();
        }
      } catch (error) {
        console.log(error);
        window.close();
      }
    },
    /**
     * 滚动方法
     * @param {Array|Object|Function} options { state: 滚动状态, type: 滚动到顶部或者底部, fn: 滚动到指定位置时调用的方法 }
     * @param {Object} disabledMap 禁用对象
     * 用法
     *  document.querySelector('.xxx').addEventListener('scroll', function () {
          util.onScroll.call(this, [
            {state: true},
            {state: true, type: 'top'},
          ]);
        });
     */
    scroll(options = [], disabledMap = {}) {
      const element = this; // element = HTML元素
      const entire = Object.prototype.toString.call(element) === '[object HTMLDocument]'; // entire = 是否为整个 document 对象
      const scrollHeight = entire ? element.documentElement.scrollHeight : element.scrollHeight; // 内容高度
      const clientHeight = entire ? element.documentElement.clientHeight : element.clientHeight; // 显示高度
      const scrollTop = entire ? (element.documentElement.scrollTop || element.body.scrollTop) : element.scrollTop; // 当前滚动条距离顶部高度

      const optionsType = Object.prototype.toString.call(options);
      let groups = [];
      if (optionsType === '[object Object]') {
        groups = [options];
      } else if (optionsType === '[object Function]') {
        groups = [{ fn: options }];
      } else if (optionsType === '[object Array]') {
        groups = options;
      }
      groups.forEach((item) => {
        const { fn = null, type = 'bottom', } = item;
        let flag = false;
        if (type === 'top') {
          flag = scrollTop === 0;
        } else if (type === 'bottom') {
          flag = scrollHeight - scrollTop <= clientHeight + 10;
        } else if (typeof type === 'number') {
          flag = scrollTop >= type;
        }
        if (flag && !disabledMap[type] && typeof fn === 'function') {
          const res = fn();
          if (res) {
            Object.keys(disabledMap).forEach((k) => {
              if (typeof res[k] !== 'undefined') {
                disabledMap[k] = res[k];
              }
            });
          }
        }
      });
    },
    /**
     * 监听滚动
     * @param {Array|Object} options { type: 滚动到顶部或者底部, fn: 滚动到指定位置时调用的方法 }
     * 用法
     * util.onScroll({ ele: document, type: 'top', fn: ()=> {} })
     * util.onScroll([
     { ele: '.xxx' },
     { ele: document.querySelector('.xxx'), fn: () => {} },
     ]);
     */
    onScroll(options = []) {
      const optionsType = Object.prototype.toString.call(options);
      if (optionsType === '[object Object]') {
        options = [options];
      } else if (optionsType === '[object Function]') {
        options = [{ fn: options }];
      } else if (optionsType !== '[object Array]') {
        options = [];
      }
      const groups = [];
      const disabledMap = {};
      options.forEach((item) => {
        const { ele = document, fn = null, type = 'bottom' } = item;
        let element;
        const eleType = Object.prototype.toString.call(ele);
        if (eleType === '[object String]') {
          element = document.querySelector(ele);
        } else if (eleType === '[object HTMLDocument]' || /^\[object HTML(.+)Element]$/.test(eleType)) {
          element = ele;
        }
        if (!element) return;

        let i = 0;
        for (; i < groups.length; i++) {
          if (groups[i].element === element) {
            break;
          }
        }
        if (i < groups.length) {
          groups[i].options.push({
            fn,
            type,
          });
        } else {
          groups.push({
            element,
            options: [{ fn, type }],
          });
        }
        disabledMap[type] = false;
      });

      groups.forEach((group) => {
        group.element.addEventListener('scroll', () => {
          this.scroll.call(group.element, group.options, disabledMap);
        });
      });
    },
    /**
     * 修复H5移动端唤醒键盘后页面样式错位的问题
     */
    fixScroll() {
      const os = this.checkOS();
      if (os === 'ios') {
        // ios端
        const speed = 1;
        setTimeout(function () {
          let currentPosition = document.documentElement.scrollTop || document.body.scrollTop;
          currentPosition -= speed;
          scrollTo(0, currentPosition); // 页面向上滚动
          currentPosition += speed;
          scrollTo(0, currentPosition); // 页面向下滚动
        }, 100);
      } else if (os === 'android') {
        // android端
      }
    },
  };

  const dataUtil = {
    /**
     * 信息脱敏
     * @param {string} str 字符串
     * @param {string} type 类型
     */
    hideSensitiveData(str, type = 'name') {
      if (type === 'mobile') {
        str = str.slice(0, 3) + '*'.repeat(str.length - 7) + str.slice(-4);
      } else if (type === 'bankCard') {
        str = str.slice(0, 6) + '*'.repeat(str.length - 10) + str.slice(-4);
      } else if (type === 'idCard') {
        str = str.slice(0, 6) + '*'.repeat(str.length - 9) + str.slice(-3);
      } else if (type === 'name') {
        str = str.length > 2 ? str.slice(0, -2) + '**' : str.length > 1 ? str.slice(0, 1) + '*' : str;
      } else if (type === 'cert') {
        str = str.slice(0, 3) + '*'.repeat(str.length - 7) + str.slice(-4);
      } else if (type === 'address') {
        str = str.slice(0, 3) + '*'.repeat(Math.min(str.length - 3, 8)) + str.slice(11);
      }
      return str;
    },
    /**
     * 将对象转换成url参数格式的字符串 精确转换
     * @param {any} data
     * @param {string} key
     * @param {boolean} encode
     * @return {string}
     */
    stringifyObj(data, key = '', encode = false) {
      let jsonString = '';
      if (data === undefined || data === null) {
        data = '';
      }
      if (typeof data !== 'object' && key === '') {
        return '';
      }
      const dataType = Object.prototype.toString.call(data);
      if (dataType === '[object Object]') {
        const dataKeys = Object.keys(data);
        dataKeys.sort();
        dataKeys.forEach((k) => {
          jsonString += this.stringifyObj(data[k], key ? `${key}[${k}]` : k, encode) + '&';
        });
      } else if (dataType === '[object Array]') {
        data.forEach((item, idx) => {
          jsonString += this.stringifyObj(item, `${key}[${idx}]`, encode) + '&';
        });
      } else {
        jsonString = `${key}=` + (encode ? encodeURIComponent(data) : data) + '&';
      }
      return jsonString.substring(0, jsonString.length - 1);
    },
    /**
     * 格式化时间
     * @param {string|number|Date|null} time 时间戳，毫秒，时间对象，格式化时间
     * @param {string} format 格式，例子：Y-M-D h:m:s.ms
     * @param {boolean} isUTC 是否转换为UTC的时间
     * @return {string}
     */
    formatTime(time, format = 'YYYY-MM-DD HH:mm:ss', isUTC = false) {
      const type = Object.prototype.toString.call(time);
      const realTime = type === '[object Date]' ? time :
        (type === '[object Number]' ? new Date(timestamp) :
          (time ? new Date(time.replace(/[.-]/g, '/')) : new Date()));

      let year, month, date, hours, minutes, seconds, milliseconds;
      if (isUTC) {
        year = realTime.getUTCFullYear();
        month = realTime.getUTCMonth() + 1;
        date = realTime.getUTCDate();
        hours = realTime.getUTCHours();
        minutes = realTime.getUTCMinutes();
        seconds = realTime.getUTCSeconds();
        milliseconds = realTime.getUTCMilliseconds();
      } else {
        year = realTime.getFullYear();
        month = realTime.getMonth() + 1;
        date = realTime.getDate();
        hours = realTime.getHours();
        minutes = realTime.getMinutes();
        seconds = realTime.getSeconds();
        milliseconds = realTime.getMilliseconds();
      }

      month = month < 10 ? '0' + month : month;
      date = date < 10 ? '0' + date : date;
      hours = hours < 10 ? '0' + hours : hours;
      minutes = minutes < 10 ? '0' + minutes : minutes;
      seconds = seconds < 10 ? '0' + seconds : seconds;
      milliseconds = milliseconds < 10 ? ('00' + milliseconds) : (milliseconds < 100 ? '0' + milliseconds : milliseconds);

      return format
        .replace(/SSS/g, milliseconds)
        .replace(/ss/g, seconds)
        .replace(/mm/g, minutes)
        .replace(/HH/g, hours)
        .replace(/DD/g, date)
        .replace(/MM/g, month)
        .replace(/YYYY/g, year);
    },
    /**
     * 文本输入过滤
     * @param {string} value 输入值
     * @param {Object} options 配置项
     * @returns {{errMsg: string, value: string}}
     */
    inputFilter(value, options = {}) {
      let { type = '', decimal = -1, max = 0 } = options;
      max = Math.floor(Number(max) || 0);
      decimal = Math.floor(Number(decimal));
      decimal = decimal === 0 ? 0 : (decimal || -1);

      let errMsg = '';
      if (value !== '') {
        if (max && value.length > max) {
          errMsg = `长度不得超过${max}位`;
          value = value.substring(0, max);
        }
        if (type === 'int') {
          const invalidReg = /[^\d]/g;
          if (value.match(invalidReg)) {
            errMsg = `请输入正确的数字`;
            value = value.replace(/[^\d]/g, '');
          }
        } else if (type === 'number') {
          const invalidReg = /[^\d.]/g;
          // 如果有非数字、小数点，或者小数点不止1个
          if (value.match(invalidReg) || value.split('.').length > 2) {
            errMsg = `请输入正确的数字`;
          }
          value = value.replace(invalidReg, ''); // 清除非数字部分
          const arr = value.split('.');
          // 如果有整数部分，或者有小数部分，则进行规则优化
          if (arr[0] !== '' || arr.length > 1) {
            // 整数部分
            let intNum = arr[0].replace(/^0/, ''); // 清除头部的0
            if (intNum === '') intNum = '0'; // 如果清除后数据为''，则赋值为0
            // 小数部分
            let digitNum = arr[1] || '';
            digitNum = decimal < 0 ? digitNum : digitNum.substring(0, decimal); // 保留小数位数，后面的全部舍去
            value = `${intNum}` + (arr.length > 1 ? `.${digitNum}` : '');
          }
        } else if (type === 'cn_name') {
          const invalidReg = /[^a-zA-Z0-9_\u4e00-\u9fa5]/g;
          if (value.match(invalidReg)) {
            errMsg = `输入内容允许包含：汉字、字母、数字、下划线'_'`;
            value = value.replace(/[^a-zA-Z0-9_\u4e00-\u9fa5]/g, '');
          }
        }
      }

      return { value, errMsg };
    },
    /**
     * 随机数 [0, max) 随机数
     * @param max
     * @returns {number} max 最大值（无法取到）
     */
    randomNum(max) {
      let num;
      if (Uint32Array && crypto && crypto.getRandomValues) {
        const arr = new Uint32Array(1);
        crypto.getRandomValues(arr);
        num = arr[0] % max;
      } else {
        num = Math.floor(Math.random() * max);
      }
      return num;
    },
    /**
     * 乱序字符串
     * @param {string} str
     * @return {string}
     */
    strShuffle(str = '') {
      const strArray = Array.from(str);
      for (let i = 0; i < strArray.length; i++) {
        const j = Math.floor(Math.random() * (i + 1));
        const temp = strArray[i];
        strArray[i] = strArray[j];
        strArray[j] = temp;
      }
      return strArray.join('');
    },
    /**
     * 生成随机字符串
     * @param {int} length
     * @param {int|null} type
     * @param {string} addChars
     * @return {string}
     */
    randomStr(length = 6, type = null, addChars = '') {
      let chars = `ABCDEFGHIJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789${addChars}`;
      switch (type) {
        case 0:
          chars = `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz${addChars}`;
          break;
        case 1:
          chars = '0123456789'.repeat(3);
          break;
        case 2:
          chars = `ABCDEFGHIJKLMNOPQRSTUVWXYZ${addChars}`;
          break;
        case 3:
          chars = `abcdefghijklmnopqrstuvwxyz${addChars}`;
          break;
        case 4:
          chars = `们以我到他会作时要动国产的一是工就年阶义发成部民可出能方进在了不和有大这主中人上为来分生对于学下级地个用同行面说种过命度革而多子后自社加小机也经力线本电高量长党得实家定深法表着水理化争现所二起政三好十战无农使性前等反体合斗路图把结第里正新开论之物从当两些还天资事队批点育重其思与间内去因件日利相由压员气业代全组数果期导平各基或月毛然如应形想制心样干都向变关问比展那它最及外没看治提五解系林者米群头意只明四道马认次文通但条较克又公孔领军流入接席位情运器并飞原油放立题质指建区验活众很教决特此常石强极土少已根共直团统式转别造切九你取西持总料连任志观调七么山程百报更见必真保热委手改管处己将修支识病象几先老光专什六型具示复安带每东增则完风回南广劳轮科北打积车计给节做务被整联步类集号列温装即毫知轴研单色坚据速防史拉世设达尔场织历花受求传口断况采精金界品判参层止边清至万确究书${addChars}`;
          break;
      }
      if (length > 10) {
        chars = type === 1 ? chars.repeat(length) : chars.repeat(5);
      }
      chars = this.strShuffle(chars);
      return chars.substr(0, length);
    },
  };

  const cryptoUtil = {
    /**
     * RSA加密
     * @param {string} str 要加密的字符串
     * @param {string} publicKey RSA公钥
     * @param {string} algorithm 算法 可选值：RSA, RSAOAEP, RSAOAEP224, RSAOAEP256, RSAOAEP384, RSAOAEP512
     * @returns {string|null} 加密后的字符串
     */
    encryptByRSA(str, publicKey, algorithm = 'RSA') {
      if (window.KEYUTIL && window.KJUR && window.hextob64) {
        const pubKey = window.KEYUTIL.getKey(publicKey);
        const encrypted = window.KJUR.crypto.Cipher.encrypt(str, pubKey, algorithm);
        return window.hextob64(encrypted);
      }
      return null;
    },
    /**
     * RSA解密
     * @param {string} encrypted 被加密的字符串
     * @param {string} privateKey RSA私钥
     * @param {string} algorithm 算法 可选值：RSA, RSAOAEP, RSAOAEP224, RSAOAEP256, RSAOAEP384, RSAOAEP512
     * @returns {string|null} 解密后的字符串
     */
    decryptByRSA(encrypted, privateKey, algorithm = 'RSA') {
      if (window.KEYUTIL && window.KJUR && window.b64tohex) {
        encrypted = window.b64tohex(encrypted);
        const prvKey = window.KEYUTIL.getKey(privateKey);
        return window.KJUR.crypto.Cipher.decrypt(encrypted, prvKey, algorithm);
      }
      return null;
    },
    /**
     * 加密字符串 支持 DES、AES 加密
     * @param {string} str 待加密字符串
     * @param {string} type 加密类型 des aes
     * @param {object} options 配置项对象 包含 { key, iv, mode, padding }
     */
    encryptStr(str, type = 'des', options = {}) {
      if (window.CryptoJS) {
        const keyHex = window.CryptoJS.enc.Utf8.parse(options.key);
        const iv = window.CryptoJS.enc.Utf8.parse(options.iv);
        const mode = options.mode || window.CryptoJS.mode.CBC;
        const padding = options.padding || window.CryptoJS.pad.Pkcs7;
        let encryptFunc;
        if (type === 'des') {
          encryptFunc = window.CryptoJS.DES.encrypt;
        } else if (type === 'aes') {
          encryptFunc = window.CryptoJS.AES.encrypt;
        }
        if (encryptFunc) {
          const encrypted = encryptFunc(str, keyHex, {
            iv,
            mode,
            padding,
          });
          return encrypted.toString();
        }
      }
      return null;
    },
    /**
     * 解密字符串 支持 DES、AES 解密
     * @param {string} str 待解密字符串
     * @param {string} type 加密类型 des aes
     * @param {object} options 配置项对象 包含 { key, iv, mode, padding }
     */
    decryptStr(str, type = 'des', options = {}) {
      if (window.CryptoJS) {
        const keyHex = window.CryptoJS.enc.Utf8.parse(options.key);
        const iv = window.CryptoJS.enc.Utf8.parse(options.iv);
        const mode = options.mode || window.CryptoJS.mode.CBC;
        const padding = options.padding || window.CryptoJS.pad.Pkcs7;
        let decryptFunc;
        if (type === 'des') {
          decryptFunc = window.CryptoJS.DES.decrypt;
        } else if (type === 'aes') {
          decryptFunc = window.CryptoJS.AES.decrypt;
        }
        if (decryptFunc) {
          const decrypted = decryptFunc(str, keyHex, {
            iv,
            mode,
            padding,
          });
          return decrypted.toString(window.CryptoJS.enc.Utf8);
        }
      }
      return null;
    },
    /**
     * 生成数据埋点签名，需引用 static/gtxy_md5.js(专有代码，不能从网上另外下载)
     * @param {Object} data 请求参数
     * @param {string} md5Key md5摘要key
     * @param {Array<string> | null} signKeys data中进行签名的key值
     * @param {string} isSort signKeys 是否进行ascii排序
     * @returns {string}
     */
    getSupDataSign(data, md5Key, signKeys = null, isSort = false) {
      let signStr = 'MD5KEY=' + md5Key;
      if (!signKeys) {
        signKeys = Object.keys(data);
      }
      if (isSort) {
        signKeys.sort();
      }
      signKeys.map((key) => {
        let v = data[key];
        const vType = Object.prototype.toString.call(v);
        if (vType === '[object Object]' || vType === '[object Array]') {
          v = JSON.stringify(v);
        }
        if (v === null) {
          v = 'null';
        } else if (typeof v === 'undefined') {
          v = 'undefined';
        }
        signStr += '&' + key.toUpperCase() + '=' + (v === undefined || v === null ? '' : v);
      });
      return hex_md5(signStr, 'activityClick').toUpperCase();
    },
    /**
     * 字符串转byte数组
     * @param str
     * @returns {Array<number>}
     */
    strToByte(str) {
      const bytes = [];
      let c, len = str.length;
      for (let i = 0; i < len; i++) {
        c = str.charCodeAt(i);
        if (c >= 0x010000 && c <= 0x10FFFF) {
          bytes.push(((c >> 18) & 0x07) | 0xF0);
          bytes.push(((c >> 12) & 0x3F) | 0x80);
          bytes.push(((c >> 6) & 0x3F) | 0x80);
          bytes.push((c & 0x3F) | 0x80);
        } else if (c >= 0x000800 && c <= 0x00FFFF) {
          bytes.push(((c >> 12) & 0x0F) | 0xE0);
          bytes.push(((c >> 6) & 0x3F) | 0x80);
          bytes.push((c & 0x3F) | 0x80);
        } else if (c >= 0x000080 && c <= 0x0007FF) {
          bytes.push(((c >> 6) & 0x1F) | 0xC0);
          bytes.push((c & 0x3F) | 0x80);
        } else {
          bytes.push(c & 0xFF);
        }
      }
      return bytes;
    },
    /**
     * byte数组转字符串
     * @param {Array<number>} arr
     * @returns {string}
     */
    byteToStr(arr) {
      let str = '';
      for (let i = 0; i < arr.length; i++) {
        const one = arr[i].toString(2),
          v = one.match(/^1+?(?=0)/);
        if (v && one.length === 8) {
          const bytesLength = v[0].length;
          let store = arr[i].toString(2).slice(7 - bytesLength);
          for (let st = 1; st < bytesLength; st++) {
            store += arr[st + i].toString(2).slice(2);
          }
          str += String.fromCharCode(parseInt(store, 2));
          i += bytesLength - 1;
        } else {
          str += String.fromCharCode(arr[i]);
        }
      }
      return str;
    },
    /**
     * RSA加密请求数据(识别最大长度，分段加密)
     * @param {string} jsonString 请求数据
     * @param {string} publicRSAKey rsa公钥
     * @returns {object}
     */
    encryptDataByRsa(jsonString, publicRSAKey) {
      const algorithmName = 'RSA';
      let encrypted,
        count,
        maxLen = 157,
        step = Math.floor(maxLen / 4), // 步长为: 最大长度/4，确保加密时长度不会过长
        stepEncryptedArr = [],
        stepJsonString,
        stepEncrypted,
        i;
      const byteData = this.strToByte(jsonString);
      if (byteData.length > maxLen) {
        // 如果超过最大长度，进行分割加密，然后合并
        count = Math.ceil(byteData.length / maxLen);
        for (i = 0; i < count; i++) {
          stepJsonString = jsonString.slice(step * i, step * (i + 1));
          stepEncrypted = this.encryptByRSA(stepJsonString, publicRSAKey, algorithmName);
          stepEncryptedArr.push(stepEncrypted);
        }
        encrypted = stepEncryptedArr.toString();
      } else {
        encrypted = this.encryptByRSA(jsonString, publicRSAKey, algorithmName);
      }
      return encrypted;
    },
  };

  const eventUtil = {
    /**
     * 监听对象值的修改
     * @param {Object} obj
     * @param {string} key
     * @param {Function} watchFn
     * @param {Boolean} deep
     * @param {this} ctx
     */
    observe(obj, key, watchFn, deep = false, ctx = null) {
      if (!ctx) ctx = this;
      let val = obj[key];
      // 判断deep是true 且 val不能为空 且 typeof val==='object'（数组内数值变化也需要深度监听）
      if (deep && val != null && typeof val === 'object') {
        Object.keys(val).forEach((childKey) => { // 遍历val对象下的每一个key
          this.observe(val, childKey, watchFn, deep, ctx); // 递归调用监听函数
        });
      }
      Object.defineProperty(obj, key, {
        configurable: true,
        enumerable: true,
        set: (value) => {
          const flag = value !== val; // 是否改变原值
          const oldVal = val;
          val = value; // 先改变原值
          // 用ctx对象调用,改变函数内this指向,以便this.data访问data内的属性值
          if (flag) {
            watchFn.call(ctx, value, oldVal); // value是新值，val是旧值
          }
          if (deep) { // 若是深度监听,重新监听该对象，以便监听其属性。
            this.observe(obj, key, watchFn, deep, ctx);
          }
        },
        get: function() {
          return val;
        }
      })
    },
    /**
     * 节流/防抖
     * @param {Function} func 事件
     * @param {number} wait 延迟时间
     * @param {boolean} immediate true:节流 false:防抖
     * @returns {function(): *}
     */
    debounce(func, wait = 500, immediate = false) {
      let timeout, result;

      return function () {
        const context = this;
        const args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
          // 节流，如果已经执行过，不再执行
          const callNow = !timeout;
          timeout = setTimeout(function () {
            timeout = null;
          }, wait);
          if (callNow) result = func.apply(context, args);
        } else {
          // 防抖，延迟执行
          timeout = setTimeout(function () {
            func.apply(context, args);
          }, wait);
        }
        return result;
      };
    },
    /**
     * 创建二维码（需引用 node-qrcode/qrcode.min.js）
     * @param {string} text 文本信息
     * @param {Object} options 配置项
     * @returns {Promise<string>} 返回二维码图片 base64 信息
     */
    createQrcode(text, options = {}) {
      return new Promise((r, j) => {
        if (window.QRCode) {
          const defaultOptions = {
            mode: 'byte', // 编码格式 numeric:[0-9]; alphanumeric:[0-9A-Z$%*+–./:]; byte:ISO/IEC-8859-1字符; kanji:基于JIS X 0208的Shift JIS系统的字符
            version: 0, // 二维码版本。如果未指定，将计算更合适的值（简单来说就是计算文本的长度，并使用合适的值，如果太小了无法生成二维码，推荐自动计算）
            errorCorrectionLevel: 'L', // 纠错级别，L,M,Q,H
            // maskPattern: 0, // 屏蔽符号的屏蔽模式。如果未指定，将计算更合适的值。可选值0-7
            // toSJISFunc: QRCode.toSJIS, // 内部用于将汉字转换为其 Shift JIS 值的辅助函数。如果需要支持汉字模式，开启此选项。需引入'tosjis.js'
            margin: 5, // 二维码留白的宽度，1=4px
            // scale: 4, // 比例。1表示每个模块 1px
            // small: false, // 输出更小的二维码。仅与终端渲染器相关
            width: 270, // 强制宽度。如果宽度太小而无法包含 qr 符号，则此选项将被忽略。优先于scale
            color: {
              dark: '#000000ff',
              light: '#ffffffff',
            },
          };
          const qrcodeOptions = Object.assign({}, defaultOptions, options);
          window.QRCode.toDataURL(text, qrcodeOptions, (err, url) => {
            if (err) {
              j(err);
            } else {
              r(url);
            }
          });
        } else {
          j('未启用二维码插件');
        }
      });
    },
    /**
     * 下载图片
     * @param {Object} options 配置项
     *                         url:图片链接，
     *                         base64:图片base64值，
     *                         canvas:画布DOM，
     *                         name:图片名称，
     *                         注：其中url、base64、canvas三选一必传
     * @returns {Promise<Object>}
     */
    downloadImage(options) {
      return new Promise((r, j) => {
        let { url: imgUrl, base64, canvas: canvasDom, name: imgName } = options;
        const download = (imgSource, name) => {
          const base64Matches = imgSource.match(/^data:image\/([a-zA-Z]+);base64,.+$/i);
          if (!base64Matches) {
            return false;
          }
          if (!name) {
            name = dataUtil.randomStr(8);
          }
          const imgName = `${name}.${base64Matches[1]}`;
          const a = document.createElement('a'); // 生成一个a元素
          const event = new MouseEvent('click'); // 创建一个单击事件
          a.download = imgName; // 设置图片名称
          a.href = imgSource; // 将生成的URL设置为a.href属性
          a.dispatchEvent(event); // 触发a的单击事件
          return { code: 0, message: 'image download success', data: imgName };
        };
        if (canvasDom) {
          // 传的是dom
          const url = canvasDom.toDataURL(`image/png`); // 将图片格式转为base64
          r(download(url, imgName));
        } else if (base64) {
          // 传的是图片base64值
          r(download(base64, imgName));
        } else if (imgUrl) {
          // 传的是图片链接
          const splitUnit = imgUrl.indexOf('?') > -1 ? '&' : '?';
          imgUrl += splitUnit + '_t=' + dataUtil.randomStr();
          const image = new Image();
          image.crossOrigin = 'anonymous'; // 解决跨域 Canvas 污染问题
          image.onload = () => {
            const canvas = document.createElement('canvas');
            canvas.width = image.width;
            canvas.height = image.height;
            const context = canvas.getContext('2d');
            context.drawImage(image, 0, 0, image.width, image.height);
            const imgBase64 = canvas.toDataURL(`image/png`); // 将图片格式转为base64
            const urlMatches = imgUrl.match(/(?:.*\/)*(([^?]+)\.([^.?]+))(?:\?.*)?/i);
            r(download(imgBase64, imgName || urlMatches[2]));
          };
          image.onerror = (e) => {
            j({ code: 500, message: 'image load fail' });
          };
          image.src = imgUrl;
        } else {
          j({ code: 500, message: 'image load fail' });
        }
      });
    },
    /**
     * 压缩图片文件
     * @param {File} imgFile 图片文件
     * @param {number} quality
     * @param {number} maxWidth
     * @returns {Promise<File>}
     */
    compressImg(imgFile, quality = 0.9, maxWidth = 750) {
      return new Promise((r) => {
        const { name: fileName, type: fileType } = imgFile;
        quality = quality < 0.3 ? 0.3 : quality;
        const URL = window.URL || window.webkitURL;
        const blob = URL.createObjectURL(imgFile);
        const img = new Image();
        img.src = blob;
        img.onload = () => {
          try {
            URL.revokeObjectURL(blob);
          } catch (_e) {}
          // 图片宽高，限制最大宽度，后等比缩小
          let { width, height } = img;
          const scale = width / height;
          if (width > maxWidth) width = maxWidth;
          height = width / scale;
          // 生成canvas
          const canvas = document.createElement('canvas');
          const ctx = canvas.getContext('2d');
          canvas.width = width;
          canvas.height = height;
          ctx.drawImage(img, 0, 0, width, height);
          // 生成base64
          const base64 = canvas.toDataURL(fileType, quality).split(',')[1];
          let uint8;
          if (window.atob) {
            let binary = window.atob(base64);
            let array = [];
            for (let i = 0; i < binary.length; i++) {
              array.push(binary.charCodeAt(i));
            }
            uint8 = new Uint8Array(array);
          } else {
            let len = base64.length * 0.75; // 转换为int8array所需长度
            const base64Tmp = base64.replace(/=*$/, ''); // 去掉=号（占位的）

            uint8 = new Uint8Array(len); //设置uint8array视图
            let p = 0;
            for (let i = 0; i < base64Tmp.length; i += 4) {
              // 每次循环 都将base64 4个字节转换为 3个uint8array 进行直接
              const array = [
                base64Map[base64Tmp[i]],
                base64Map[base64Tmp[i + 1]],
                base64Map[base64Tmp[i + 2]],
                base64Map[base64Tmp[i + 3]],
              ];
              // 假设数据array 数据 00101011 00101111 00110011 00110001
              uint8[p++] = (array[0] << 2) | (array[1] >> 4);
              // 上面的操作 arr[0]向左边移动2位 变为10101100
              // arr[1] 向右移动4位：00000010
              // | 为'与'操作: 10101110
              uint8[p++] = (array[1] << 4) | (array[2] >> 2);
              uint8[p++] = (array[2] << 6) | array[3];
            }
          }
          r(new File([uint8], fileName, { type: fileType }));
        };
        img.onerror = (_e) => {
          r(imgFile);
        };
      });
    },
    /**
     * 复制文本
     * @param {string} text
     * @returns {boolean}
     */
    copyText(text) {
      var textarea = document.createElement('textarea');
      textarea.setAttribute('readonly', 'readonly');
      textarea.value = text;
      textarea.style.opacity = '0';
      textarea.style.position = 'fixed';
      textarea.style.top = '-9999px';
      textarea.style.left = '-9999px';
      document.body.appendChild(textarea);
      textarea.select();
      var res = document.execCommand('copy'); //复制选中的文字到剪贴板
      textarea.remove();
      return res;
    },
  };

  const dialogUtil = {
    // loading弱提示列表
    loadingToastList: [],
    /**
     * 显示loading
     * @param {string} msg 提示内容
     * @param {boolean} mask 遮罩层
     */
    showLoading(msg = '', mask = true) {
      if (window.vant && window.vant.Toast) {
        window.vant.Toast.allowMultiple();
        this.loadingToastList.push({
          type: 'vant',
          instance: window.vant.Toast.loading({
            message: msg,
            duration: 0,
            forbidClick: mask,
          }),
        });
      } else if (window.TDesign && window.TDesign.Toast) {
        this.loadingToastList.push({
          type: 'TDesign',
          instance: window.TDesign.Toast.loading({
            message: msg,
            duration: 0,
            direction: 'column',
            showOverlay: true,
            overlayProps: { style: 'background:transparent' },
          }),
        });
      } else if (window.layer && window.layer.open) {
        this.loadingToastList.push({
          type: 'layer',
          index: window.layer.open({
            content: msg,
            type: 2,
            shade: mask ? 'background-color: rgba(0, 0, 0, 0.3)' : false,
            shadeClose: false,
          }),
        });
      }
    },
    /**
     * 隐藏loading
     */
    hideLoading() {
      try {
        let TDesignLoading = false;
        for (const item of this.loadingToastList) {
          if (item.type === 'vant') {
            item.instance.close();
          } else if (item.type === 'TDesign') {
            TDesignLoading = true;
          } else if (item.type === 'layer') {
            window.layer.close(item.index);
          }
        }
        if (TDesignLoading && window.TDesign.Toast) {
          window.TDesign.Toast.clear();
        }
      } catch (e) {}
      this.loadingToastList = [];
    },
    /**
     * 弱弹窗提示
     * @param {string} msg 提示内容
     * @param {number} duration 持续时间
     * @param {function} end 结束时回调函数
     * @param {string} position 展示位置 top/middle/bottom
     */
    showToast(msg, duration = 2, end = null, position = 'bottom') {
      if (window.vant && window.vant.Toast) {
        window.vant.Toast({
          position: position,
          message: msg,
          duration: duration * 1000,
          onClose: () => {
            if (typeof end === 'function') {
              end();
            }
          },
        });
      } else if (window.TDesign && window.TDesign.Toast) {
        window.TDesign.Toast({
          placement: position,
          message: msg,
          duration: duration * 1000,
          onClose: () => {
            if (typeof end === 'function') {
              end();
            }
          },
        });
        if (typeof end === 'function') {
          setTimeout(end, duration * 1000);
        }
      } else if (window.layer && window.layer.open) {
        window.layer.open({
          content: msg,
          skin: 'msg',
          time: duration,
          end: () => {
            if (typeof end === 'function') {
              end();
            }
          },
        });
      } else {
        alert(msg);
        if (typeof end === 'function') {
          end();
        }
      }
    },
    /**
     * 弹窗提示
     * @param {string} msg
     */
    showAlert(msg) {
      if (window.vant && window.vant.Dialog) {
        window.vant.Dialog.alert({
          message: msg,
          confirmButtonText: '确定',
        });
      } else if (window.TDesign && window.TDesign.Dialog) {
        window.TDesign.Dialog.alert({
          content: msg,
          cancelBtn: null,
          confirmBtn: '确定',
        });
      } else if (window.layer && window.layer.open) {
        window.layer.open({
          content: msg,
          btn: '确定',
        });
      } else {
        alert(msg);
      }
    },
  };

  const util = Object.assign({}, pageUtil, dataUtil, cryptoUtil, eventUtil, dialogUtil);

  // 暴露方法
  window.util = util;
  return util;
});
